﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using Microsoft.SharePoint.Administration;
using Microsoft.SharePoint;
using System.Collections;
using System.Xml.Serialization;
using System.IO;

namespace SPPropertyBagManagement.Code
{
    public static class PropertyManager
    {
        /// <summary>
        /// Gets the property.
        /// </summary>
        /// <param name="scope">The scope.</param>
        /// <param name="scopeObjectId">The scope object id.</param>
        /// <param name="itemId"> </param>
        /// <param name="propertyId">The property id.</param>
        /// <returns></returns>
        /// <exception cref="System.ArgumentException">scopeObjectId</exception>
        /// <exception cref="System.ArgumentOutOfRangeException">scope</exception>
        public static Property GetProperty(ScopeType scope, Guid? scopeObjectId, int? itemId, string propertyId)
        {
            switch (scope)
            {
                case ScopeType.ListItem:
                    if (scopeObjectId == null)
                    {
                        throw new ArgumentException("scopeObjectId");
                    }

                    if (itemId == null)
                    {
                        throw new ArgumentException("itemId");
                    }

                    return GetListItemProperty(scopeObjectId.Value, itemId.Value, propertyId);
                case ScopeType.List:
                    if (scopeObjectId == null)
                    {
                        throw new ArgumentException("scopeObjectId");
                    }

                    return GetListProperty(scopeObjectId.Value, propertyId);
                case ScopeType.Web:
                    return GetWebProperty(propertyId);
                case ScopeType.WebApplication:
                    if (scopeObjectId == null)
                    {
                        throw new ArgumentException("scopeObjectId");
                    }

                    return GetWebAppProperty(scopeObjectId.Value, propertyId);
                case ScopeType.Farm:
                    return GetFarmProperty(propertyId);
                default:
                    throw new ArgumentOutOfRangeException("scope");
            }
        }

        /// <summary>
        /// Gets the list property.
        /// </summary>
        /// <param name="listId">The list id.</param>
        /// <param name="propertyId">The property id.</param>
        /// <returns></returns>
        public static Property GetListProperty(Guid listId, string propertyId)
        {
            var value = SPContext.Current.Web.Lists[listId].RootFolder.Properties[propertyId];

            if (value == null)
            {
                return null;
            }

            Property prop = new Property();
            prop.PropertyName = propertyId;
            prop.Scope = ScopeType.List;
            prop.ScopeObjectId = listId;
            prop.SupportsEditing = IsSupportedPropertyValueType(value.GetType());
            prop.TypeCode = Type.GetTypeCode(value.GetType());
            prop.PropertyValue = value;

            return prop;
        }

        /// <summary>
        /// Gets the list item property.
        /// </summary>
        /// <param name="listId">The list id.</param>
        /// <param name="itemId">The item id.</param>
        /// <param name="propertyId">The property id.</param>
        /// <returns></returns>
        public static Property GetListItemProperty(Guid listId, int itemId, string propertyId)
        {
            var value = SPContext.Current.Web.Lists[listId].GetItemById(itemId).Properties[propertyId];

            if (value == null)
            {
                return null;
            }

            Property prop = new Property();
            prop.PropertyName = propertyId;
            prop.ListItemId = itemId;
            prop.Scope = ScopeType.ListItem;
            prop.ScopeObjectId = listId;
            prop.SupportsEditing = IsSupportedPropertyValueType(value.GetType());
            prop.TypeCode = Type.GetTypeCode(value.GetType());
            prop.PropertyValue = value;

            return prop;
        }

        /// <summary>
        /// Gets the web property.
        /// </summary>
        /// <param name="propertyId">The property id.</param>
        /// <returns></returns>
        public static Property GetWebProperty(string propertyId)
        {
            var value = SPContext.Current.Web.AllProperties[propertyId];

            if (value == null)
            {
                return null;
            }

            Property prop = new Property();
            prop.PropertyName = propertyId;
            prop.Scope = ScopeType.Web;
            prop.ScopeObjectId = SPContext.Current.Web.ID;
            prop.SupportsEditing = IsSupportedPropertyValueType(value.GetType());
            prop.TypeCode = Type.GetTypeCode(value.GetType());
            prop.PropertyValue = value;

            return prop;
        }

        /// <summary>
        /// Gets the web app property.
        /// </summary>
        /// <param name="guid">The GUID.</param>
        /// <param name="propertyId">The property id.</param>
        /// <returns></returns>
        internal static Property GetWebAppProperty(Guid guid, string propertyId)
        {
            var webAppLst = new List<SPWebApplication>();
            webAppLst.AddRange(SPWebService.AdministrationService.WebApplications);
            webAppLst.AddRange(SPWebService.ContentService.WebApplications);

            var myWebApp = from SPWebApplication w in webAppLst
                           where w.Id == guid
                           select w;

            var myWebAppObj = myWebApp.First();

            var value = myWebAppObj.Properties[propertyId];

            if (value == null)
            {
                return null;
            }

            Property prop = new Property();
            prop.PropertyName = propertyId;
            prop.Scope = ScopeType.WebApplication;
            prop.ScopeObjectId = myWebAppObj.Id;
            prop.SupportsEditing = IsSupportedPropertyValueType(value.GetType());
            prop.TypeCode = Type.GetTypeCode(value.GetType());
            prop.PropertyValue = value;

            return prop;
        }

        /// <summary>
        /// Gets the farm property.
        /// </summary>
        /// <param name="propertyId">The property id.</param>
        /// <returns></returns>
        internal static Property GetFarmProperty(string propertyId)
        {
            var value = SPFarm.Local.Properties[propertyId];

            if (value == null)
            {
                return null;
            }

            Property prop = new Property();
            prop.PropertyName = propertyId;
            prop.Scope = ScopeType.Farm;
            prop.ScopeObjectId = SPFarm.Local.Id;
            prop.SupportsEditing = IsSupportedPropertyValueType(value.GetType());
            prop.TypeCode = Type.GetTypeCode(value.GetType());
            prop.PropertyValue = value;

            return prop;
        }

        /// <summary>
        /// Gets the property list.
        /// </summary>
        /// <param name="scope">The scope.</param>
        /// <param name="objectId">The object id.</param>
        /// <param name="itemObjectId"> </param>
        /// <returns></returns>
        /// <exception cref="System.ArgumentException">objectId</exception>
        internal static List<PropertyGridItem> GetProperties(ScopeType scope, Guid? objectId, int? itemObjectId)
        {
            if (scope == ScopeType.Farm)
            {
                return ConvertHashtableToPropertyGridItemCollection(SPFarm.Local.Properties);
            }

            if (scope == ScopeType.Web)
            {
                return ConvertHashtableToPropertyGridItemCollection(SPContext.Current.Web.AllProperties);
            }

            if (scope == ScopeType.List)
            {
                if (objectId == null)
                {
                    throw new ArgumentException("objectId");
                }

                return ConvertHashtableToPropertyGridItemCollection(SPContext.Current.Web.Lists[objectId.Value].RootFolder.Properties);
            }

            if (scope == ScopeType.WebApplication)
            {
                var webAppLst = new List<SPWebApplication>();
                webAppLst.AddRange(SPWebService.AdministrationService.WebApplications);
                webAppLst.AddRange(SPWebService.ContentService.WebApplications);

                var myWebApp = from SPWebApplication w in webAppLst
                               where w.Id == objectId
                               select w;

                return ConvertHashtableToPropertyGridItemCollection(myWebApp.First().Properties);
            }

            if (scope == ScopeType.ListItem)
            {
                if (objectId == null)
                {
                    throw new ArgumentException("objectId");
                }

                if (itemObjectId == null)
                {
                    throw new ArgumentException("itemObjectId");
                }

                return ConvertHashtableToPropertyGridItemCollection(SPContext.Current.Web.Lists[objectId.Value].GetItemById(itemObjectId.Value).Properties);
            }

            throw new ArgumentOutOfRangeException("scope");
        }

        /// <summary>
        /// Converts the hashtable to property grid item collection.
        /// </summary>
        /// <param name="propertiesTable">The properties table.</param>
        /// <returns></returns>
        internal static List<PropertyGridItem> ConvertHashtableToPropertyGridItemCollection(Hashtable propertiesTable)
        {
            var result = new List<PropertyGridItem>();

            if (propertiesTable == null || propertiesTable.Count == 0)
            {
                return result;
            }

            foreach (var key in propertiesTable.Keys)
            {
                if (key == null)
                {
                    continue;
                }

                PropertyGridItem prop = new PropertyGridItem();

                prop.PropertyName = key;
                prop.NameTypeCode = Type.GetTypeCode(key.GetType());
                prop.IsPropertyNameSupported = IsSupportedPropertyNameType(key.GetType());

                prop.PropertyValue = propertiesTable[key];

                if (prop.PropertyValue == null || prop.PropertyValue.ToString().Trim().Length == 0)
                {
                    prop.IsPropertyValueSupported = true;

                    //no value - we cannot use Int32 or DateTime here so we will use String as other types cannot be parsed from empty or null value
                    prop.ValueTypeCode = TypeCode.String;
                }
                else
                {
                    prop.IsPropertyValueSupported = IsSupportedPropertyValueType(prop.PropertyValue.GetType());
                    prop.ValueTypeCode = Type.GetTypeCode(prop.PropertyValue.GetType());
                }

                result.Add(prop);
            }

            result = result.OrderBy(x => x.PropertyName).ToList();

            return result;
        }

        /// <summary>
        /// Gets the supported property name code types.
        /// </summary>
        /// <returns></returns>
        internal static IEnumerable<TypeCode> GetSupportedPropertyNameCodeTypes()
        {
            var supportedTypeCodes = new List<TypeCode>();
            supportedTypeCodes.Add(TypeCode.String);

            return supportedTypeCodes;
        }

        /// <summary>
        /// Gets the supported property value code types.
        /// </summary>
        /// <returns></returns>
        internal static IEnumerable<TypeCode> GetSupportedPropertyValueCodeTypes()
        {
            var supportedTypeCodes = new List<TypeCode>();
            supportedTypeCodes.Add(TypeCode.DateTime);
            supportedTypeCodes.Add(TypeCode.Int32);
            supportedTypeCodes.Add(TypeCode.String);

            return supportedTypeCodes;
        }

        /// <summary>
        /// Determines whether [is supported property name type] [the specified type].
        /// </summary>
        /// <param name="type">The type.</param>
        /// <returns>
        ///   <c>true</c> if [is supported property name type] [the specified type]; otherwise, <c>false</c>.
        /// </returns>
        internal static bool IsSupportedPropertyNameType(Type type)
        {
            if (type == null)
            {
                return false;
            }

            var supportedCodes = GetSupportedPropertyNameCodeTypes();

            if (supportedCodes.Contains(Type.GetTypeCode(type)))
            {
                return true;
            }

            return false;
        }

        /// <summary>
        /// Determines whether [is supported property value type] [the specified type].
        /// </summary>
        /// <param name="type">The type.</param>
        /// <returns>
        ///   <c>true</c> if [is supported property value type] [the specified type]; otherwise, <c>false</c>.
        /// </returns>
        internal static bool IsSupportedPropertyValueType(Type type)
        {
            if (type == null)
            {
                return false;
            }

            var supportedCodes = GetSupportedPropertyValueCodeTypes();

            if (supportedCodes.Contains(Type.GetTypeCode(type)))
            {
                return true;
            }

            return false;
        }

        /// <summary>
        /// Saves the property.
        /// </summary>
        /// <param name="property">The property.</param>
        /// <param name="isNew">Is new?</param>
        /// <exception cref="System.ArgumentNullException">property</exception>
        /// <exception cref="System.ArgumentOutOfRangeException"></exception>
        public static void SaveProperty(Property property, bool isNew)
        {
            if (property == null)
            {
                throw new ArgumentNullException("property");
            }

            if (isNew && property.ToBeRemoved)
            {
                throw new ArgumentException("Property cannot be new and marked for removal at one time.");
            }

            if (isNew)
            {
                var exists = DoesPropertyExist(property.Scope, property.ScopeObjectId, property.ListItemId, property.PropertyName);

                if (exists)
                {
                    throw new Exception("Specified property already exists: " + property.PropertyName);
                }
            }

            if (!IsValueValidType(property.PropertyValue, property.TypeCode))
            {
                throw new ArgumentException("Invalid property value. Must be " + property.TypeCode + ".");
            }

            switch (property.Scope)
            {
                case ScopeType.ListItem:
                    SaveListItemProperty(property);
                    break;
                case ScopeType.List:
                    SaveListProperty(property);
                    break;
                case ScopeType.Web:
                    SaveWebProperty(property);
                    break;
                case ScopeType.WebApplication:
                    SaveWebAppProperty(property);
                    break;
                case ScopeType.Farm:
                    SaveFarmProperty(property);
                    break;
                default:
                    throw new ArgumentOutOfRangeException();
            }
        }

        /// <summary>
        /// Determines whether [is value valid type] [the specified value].
        /// </summary>
        /// <param name="value">The value.</param>
        /// <param name="typeCode">The type code.</param>
        /// <returns>
        ///   <c>true</c> if [is value valid type] [the specified value]; otherwise, <c>false</c>.
        /// </returns>
        /// <exception cref="System.ArgumentOutOfRangeException">typeCode</exception>
        private static bool IsValueValidType(object value, TypeCode typeCode)
        {
            if (value == null || value.ToString().Trim().Length == 0)
            {
                return true;
            }

            switch (typeCode)
            {
                case TypeCode.Int32:
                    int x;
                    return Int32.TryParse(value.ToString().Trim(), out x);
                case TypeCode.DateTime:
                    DateTime dt;
                    return DateTime.TryParse(value.ToString().Trim(), out dt);
                case TypeCode.String:
                    return true;
                default:
                    throw new ArgumentOutOfRangeException("typeCode");
            }
        }

        /// <summary>
        /// Saves the farm property.
        /// </summary>
        /// <param name="property">The property.</param>
        /// <exception cref="System.ArgumentNullException">property</exception>
        /// <exception cref="System.ArgumentException">Invalid property value type.</exception>
        private static void SaveFarmProperty(Property property)
        {
            if (property == null)
            {
                throw new ArgumentNullException("property");
            }

            var farm = SPFarm.Local;


            if (property.ToBeRemoved)
            {
                farm.Properties.Remove(property.PropertyName);
            }
            else
            {
                farm.Properties[property.PropertyName.Trim()] = GetPropertyValueAsRequiredType(property.PropertyValue, property.TypeCode).ToString();
            }

            farm.Update(true);
        }

        /// <summary>
        /// Saves the web app property.
        /// </summary>
        /// <param name="property">The property.</param>
        /// <exception cref="System.ArgumentNullException">property</exception>
        /// <exception cref="System.ArgumentException">Invalid property value type.</exception>
        private static void SaveWebAppProperty(Property property)
        {
            if (property == null)
            {
                throw new ArgumentNullException("property");
            }

            var webAppLst = new List<SPWebApplication>();
            webAppLst.AddRange(SPWebService.AdministrationService.WebApplications);
            webAppLst.AddRange(SPWebService.ContentService.WebApplications);

            var myWebApp = from SPWebApplication w in webAppLst
                           where w.Id == property.ScopeObjectId
                           select w;

            var myWebAppObj = myWebApp.First();

            if (property.ToBeRemoved)
            {
                myWebAppObj.Properties.Remove(property.PropertyName);
            }
            else
            {
                myWebAppObj.Properties[property.PropertyName.Trim()] = GetPropertyValueAsRequiredType(property.PropertyValue, property.TypeCode).ToString();
            }

            myWebAppObj.Update(true);
        }

        /// <summary>
        /// Saves the web property.
        /// </summary>
        /// <param name="property">The property.</param>
        /// <exception cref="System.ArgumentNullException">property</exception>
        /// <exception cref="System.ArgumentException">Invalid property value type.</exception>
        private static void SaveWebProperty(Property property)
        {
            if (property == null)
            {
                throw new ArgumentNullException("property");
            }

            var web = SPContext.Current.Web;
            var aau = web.AllowUnsafeUpdates;
            web.AllowUnsafeUpdates = true;

            if (property.ToBeRemoved)
            {
                web.Properties[property.PropertyName.Trim()] = null;
            }
            else
            {
                web.Properties[property.PropertyName.Trim()] = property.PropertyValue == null || property.PropertyValue.ToString().Trim().Length == 0 ? string.Empty : GetPropertyValueAsRequiredType(property.PropertyValue, property.TypeCode).ToString();    
            }
            
            web.Properties.Update();
            web.AllowUnsafeUpdates = aau;
            web.Update();
        }

        /// <summary>
        /// Saves the list property.
        /// </summary>
        /// <param name="property">The property.</param>
        /// <exception cref="System.ArgumentNullException">property</exception>
        /// <exception cref="System.ArgumentException">property.ScopeObjectId</exception>
        private static void SaveListProperty(Property property)
        {
            if (property == null)
            {
                throw new ArgumentNullException("property");
            }

            if (property.ScopeObjectId == null)
            {
                throw new ArgumentException("property.ScopeObjectId");
            }

            var list = SPContext.Current.Web.Lists[property.ScopeObjectId.Value];
            var rootFolder = list.RootFolder;

            if (property.ToBeRemoved)
            {
                rootFolder.Properties[property.PropertyName.Trim()] = null;
            }
            else
            {
                rootFolder.Properties[property.PropertyName.Trim()] = property.PropertyValue == null || property.PropertyValue.ToString().Trim().Length == 0 ? string.Empty : GetPropertyValueAsRequiredType(property.PropertyValue, property.TypeCode);
            }

            list.RootFolder.Update();
        }

        /// <summary>
        /// Saves the list item property.
        /// </summary>
        /// <param name="property">The property.</param>
        /// <exception cref="System.ArgumentNullException">property</exception>
        /// <exception cref="System.ArgumentException">property.ScopeObjectId</exception>
        private static void SaveListItemProperty(Property property)
        {
            if (property == null)
            {
                throw new ArgumentNullException("property");
            }

            if (property.ScopeObjectId == null)
            {
                throw new ArgumentException("property.ScopeObjectId");
            }

            if (property.ListItemId == null)
            {
                throw new ArgumentException("property.ListItemId");
            }

            var list = SPContext.Current.Web.Lists[property.ScopeObjectId.Value];
            var listItem = list.GetItemById(property.ListItemId.Value);

            if (property.ToBeRemoved)
            {
                listItem.Properties[property.PropertyName.Trim()] = null;
            }
            else
            {
                listItem.Properties[property.PropertyName.Trim()] = property.PropertyValue == null || property.PropertyValue.ToString().Trim().Length == 0 ? string.Empty : GetPropertyValueAsRequiredType(property.PropertyValue, property.TypeCode);
            }

            listItem.SystemUpdate();
        }

        /// <summary>
        /// Gets the type of the property value as required.
        /// </summary>
        /// <param name="value">The value.</param>
        /// <param name="type">The type.</param>
        /// <returns></returns>
        /// <exception cref="System.ArgumentOutOfRangeException">type</exception>
        private static object GetPropertyValueAsRequiredType(object value, TypeCode type)
        {
            if (value == null || value.ToString().Trim().Length == 0)
            {
                return string.Empty;
            }

            switch (type)
            {
                case TypeCode.Int32:
                    return Int32.Parse(value.ToString().Trim());
                case TypeCode.DateTime:
                    return DateTime.Parse(value.ToString().Trim());
                case TypeCode.String:
                    return value;
                default:
                    throw new ArgumentOutOfRangeException("type");
            }
        }

        /// <summary>
        /// Doeses the property exist.
        /// </summary>
        /// <param name="scope">The scope.</param>
        /// <param name="objectId">The object id.</param>
        /// <param name="itemId"> </param>
        /// <param name="propertyName">Name of the property.</param>
        /// <returns></returns>
        /// <exception cref="System.ArgumentNullException">propertyName</exception>
        private static bool DoesPropertyExist(ScopeType scope, Guid? objectId, int? itemId, string propertyName)
        {
            if (propertyName == null || propertyName.Trim().Length == 0)
            {
                throw new ArgumentNullException("propertyName");
            }

            return GetProperties(scope, objectId, itemId).Select(x => x.PropertyName).Contains(propertyName);
        }

        /// <summary>
        /// Gets the properties as XML.
        /// </summary>
        /// <param name="scope">The scope.</param>
        /// <param name="objectId">The object id.</param>
        /// <param name="itemId"> </param>
        /// <returns></returns>
        public static string GetPropertiesAsXML(ScopeType scope, Guid? objectId, int? itemId)
        {
            var props = GetProperties(scope, objectId, itemId);

            if (props == null || props.Count == 0)
            {
                return new XmlDocument().InnerXml;
            }

            string xml;

            XmlSerializer xmlSer = new XmlSerializer(props.GetType());

            using (MemoryStream ms = new MemoryStream())
            {
                xmlSer.Serialize(ms, props);
                var bytes = ms.GetBuffer();
                xml = Encoding.UTF8.GetString(bytes);
            }

            return xml;
        }
    }
}
